Micron Document
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| SparkN0de-git | SparkN0de |
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------


Commit 2db12e0bde7ecc92688a8cc2e37ff71bab4d290f


Parents : d444daa
Author : Ivan <ivan@quad4.io>
Signature : Signature validation error
Date : 2026-04-16T00:52:09-05:00

feat(electron, frontend, tests): update Linux app name handling, add UI transparency options, and improve message scrolling tests; refactor icon path retrieval and update license artifact generation logic

Changes
Diff

diff --git a/electron/main.js b/electron/main.js
index 5ce1ab64..156da78a 100644
--- a/electron/main.js
+++ b/electron/main.js
@@ -68,6 +68,10 @@ if (process.argv.includes("--disable-gpu") || process.argv.includes("--disable-s
app.disableHardwareAcceleration();
}
+if (process.platform === "linux") {
+ app.setName("reticulum-meshchatx");
+}
+
// Protocol registration
if (process.defaultApp) {
if (process.argv.length >= 2) {
@@ -411,7 +415,7 @@ function formatRenderProcessGoneDetails(details) {
exitCode: details.exitCode,
},
null,
- 2,
+ 2
);
}
@@ -441,12 +445,14 @@ function getDefaultReticulumConfigDir() {
return path.join(app.getPath("home"), ".reticulum");
}
-function createTray() {
+function getAppIconPath() {
const iconPath = path.join(__dirname, "build", "icon.png");
const fallbackIconPath = path.join(__dirname, "assets", "images", "logo.png");
- const trayIcon = fs.existsSync(iconPath) ? iconPath : fallbackIconPath;
+ return fs.existsSync(iconPath) ? iconPath : fallbackIconPath;
+}
- tray = new Tray(trayIcon);
+function createTray() {
+ tray = new Tray(getAppIconPath());
const contextMenu = Menu.buildFromTemplate([
{
label: "Show App",
@@ -523,10 +529,12 @@ app.whenReady().then(async () => {
const shouldLaunchHeadless = userProvidedArguments.includes("--headless");
if (!shouldLaunchHeadless) {
+ const appIconPath = getAppIconPath();
// create browser window
mainWindow = new BrowserWindow({
width: 1500,
height: 800,
+ icon: appIconPath,
webPreferences: {
// used to inject logging over ipc
preload: path.join(__dirname, "preload.js"),

diff --git a/meshchatx/src/frontend/js/GlobalState.js b/meshchatx/src/frontend/js/GlobalState.js
index 8c4c82ee..3d1d1838 100644
--- a/meshchatx/src/frontend/js/GlobalState.js
+++ b/meshchatx/src/frontend/js/GlobalState.js
@@ -24,6 +24,9 @@ const globalState = reactive({
nomad_render_html_enabled: true,
nomad_render_plaintext_enabled: true,
nomad_default_page_path: "/page/index.mu",
+ ui_transparency: 0,
+ ui_glass_enabled: true,
+ message_list_virtualization: true,
},
});

diff --git a/meshchatx/src/frontend/theme/designTokens.js b/meshchatx/src/frontend/theme/designTokens.js
index 605f7fdb..c2198f1f 100644
--- a/meshchatx/src/frontend/theme/designTokens.js
+++ b/meshchatx/src/frontend/theme/designTokens.js
@@ -21,6 +21,8 @@ export const MESHCHAT_THEME_VARIABLES_LIGHT = {
"--mc-focus-border": "#60a5fa",
"--mc-accent": "#2563eb",
"--mc-accent-hover": "#3b82f6",
+ "--mc-action-primary": "#2563eb",
+ "--mc-action-primary-hover": "#3b82f6",
"--mc-scrollbar-thumb": "#94a3b8",
"--mc-scrollbar-track": "#e2e8f0",
"--mc-scrollbar-thumb-border": "#e2e8f0",
@@ -72,10 +74,12 @@ export const MESHCHAT_THEME_VARIABLES_DARK = {
"--mc-text-secondary": "#ffffff",
"--mc-text-muted": "#9ca3af",
"--mc-text-label": "#e5e7eb",
- "--mc-focus": "#60a5fa",
- "--mc-focus-border": "#60a5fa",
- "--mc-accent": "#2563eb",
+ "--mc-focus": "#3b82f6",
+ "--mc-focus-border": "#3b82f6",
+ "--mc-accent": "#60a5fa",
"--mc-accent-hover": "#3b82f6",
+ "--mc-action-primary": "#2563eb",
+ "--mc-action-primary-hover": "#3b82f6",
"--mc-scrollbar-thumb": "#52525b",
"--mc-scrollbar-track": "#18181b",
"--mc-scrollbar-thumb-border": "#18181b",
@@ -133,6 +137,8 @@ export function tailwindSemanticColorExtend() {
"fg-label": "var(--mc-text-label)",
accent: "var(--mc-accent)",
"accent-hover": "var(--mc-accent-hover)",
+ "action-primary": "var(--mc-action-primary)",
+ "action-primary-hover": "var(--mc-action-primary-hover)",
focus: "var(--mc-focus)",
glass: "var(--mc-glass-surface)",
"surface-muted": "var(--mc-surface-muted)",
@@ -190,7 +196,7 @@ export function vuetifyThemesFromTokens() {
colors: {
background: L["--mc-canvas"],
surface: L["--mc-surface"],
- primary: L["--mc-accent"],
+ primary: L["--mc-action-primary"],
secondary: "#475569",
error: L["--mc-error"],
info: L["--mc-info"],
@@ -203,7 +209,7 @@ export function vuetifyThemesFromTokens() {
colors: {
background: D["--mc-canvas"],
surface: D["--mc-surface"],
- primary: D["--mc-accent"],
+ primary: D["--mc-action-primary"],
secondary: "#94a3b8",
error: D["--mc-error"],
info: D["--mc-info"],

diff --git a/scripts/build-backend.js b/scripts/build-backend.js
index 5bd7b832..615f791b 100755
--- a/scripts/build-backend.js
+++ b/scripts/build-backend.js
@@ -59,6 +59,71 @@ function generateManifest(buildDir, manifestPath) {
console.log(`Manifest saved to ${manifestPath} (${Object.keys(manifest.files).length} files)`);
}
+function failOnSpawnResult(stepName, result) {
+ if (result.error) {
+ throw result.error;
+ }
+
+ if (result.signal) {
+ console.error(`${stepName} was terminated by signal ${result.signal}.`);
+ if (result.signal === "SIGKILL") {
+ console.error(
+ "Build process was force-killed (often OOM killer). This is likely memory pressure, not a normal script error."
+ );
+ }
+ process.exit(1);
+ }
+
+ if (result.status !== 0) {
+ console.error(`${stepName} exited with status ${result.status}.`);
+ process.exit(result.status || 1);
+ }
+}
+
+function fileMtimeMs(filePath) {
+ if (!fs.existsSync(filePath)) {
+ return null;
+ }
+ return fs.statSync(filePath).mtimeMs;
+}
+
+function shouldRefreshLicenseArtifacts(repoRoot) {
+ const forceRefresh =
+ process.env.MESHCHATX_FORCE_LICENSE_ARTIFACTS === "1" ||
+ process.env.MESHCHATX_FORCE_LICENSE_ARTIFACTS === "true";
+ if (forceRefresh) {
+ return true;
+ }
+
+ const dataDir = path.join(repoRoot, "meshchatx", "src", "backend", "data");
+ const noticesPath = path.join(dataDir, "THIRD_PARTY_NOTICES.txt");
+ const frontendLicensesPath = path.join(dataDir, "licenses_frontend.json");
+
+ const outputTimes = [fileMtimeMs(noticesPath), fileMtimeMs(frontendLicensesPath)];
+ if (outputTimes.some((t) => t == null)) {
+ return true;
+ }
+ const oldestOutput = Math.min(...outputTimes);
+
+ const inputFiles = [
+ path.join(repoRoot, "pyproject.toml"),
+ path.join(repoRoot, "poetry.lock"),
+ path.join(repoRoot, "package.json"),
+ path.join(repoRoot, "pnpm-lock.yaml"),
+ path.join(repoRoot, "meshchatx", "src", "backend", "licenses_collector.py"),
+ ];
+
+ let newestInput = 0;
+ for (const inputFile of inputFiles) {
+ const mtime = fileMtimeMs(inputFile);
+ if (mtime != null && mtime > newestInput) {
+ newestInput = mtime;
+ }
+ }
+
+ return newestInput >= oldestOutput;
+}
+
try {
const platform = process.env.PLATFORM || process.platform;
const arch = process.env.ARCH || process.arch;
@@ -103,17 +168,17 @@ try {
spawnArgs = ["-x86_64", cmd, ...licensesArgs];
}
- console.log("Generating embedded third-party license artifacts...");
- const licensesResult = spawnSync(spawnCmd, spawnArgs, {
- stdio: "inherit",
- shell: false,
- env: env,
- });
- if (licensesResult.error) {
- throw licensesResult.error;
- }
- if (licensesResult.status !== 0) {
- process.exit(licensesResult.status || 1);
+ const repoRoot = path.join(__dirname, "..");
+ if (shouldRefreshLicenseArtifacts(repoRoot)) {
+ console.log("Generating embedded third-party license artifacts...");
+ const licensesResult = spawnSync(spawnCmd, spawnArgs, {
+ stdio: "inherit",
+ shell: false,
+ env: env,
+ });
+ failOnSpawnResult("License artifact generation", licensesResult);
+ } else {
+ console.log("Skipping license artifact generation (artifacts are up to date).");
}
spawnCmd = cmd;
@@ -123,17 +188,13 @@ try {
spawnArgs = ["-x86_64", cmd, ...args];
}
+ console.log("Running cx_Freeze backend build...");
const result = spawnSync(spawnCmd, spawnArgs, {
stdio: "inherit",
shell: false,
env: env,
});
- if (result.error) {
- throw result.error;
- }
- if (result.status !== 0) {
- process.exit(result.status || 1);
- }
+ failOnSpawnResult("Backend build", result);
if (fs.existsSync(buildDir)) {
if (isDarwin) {

diff --git a/tests/backend/test_lxst_integration.py b/tests/backend/test_lxst_integration.py
index 05bf7b1c..52a49ec4 100644
--- a/tests/backend/test_lxst_integration.py
+++ b/tests/backend/test_lxst_integration.py
@@ -133,7 +133,11 @@ def test_lxst_switch_profile_updates_codec_and_frame_time(monkeypatch):
telephone = LXSTTelephony.Telephone(identity)
telephone.call_status = LXSTTelephony.Signalling.STATUS_ESTABLISHED
- telephone.active_call = SimpleNamespace(profile=LXSTTelephony.Profiles.QUALITY_MEDIUM, filters=[], packetizer=MagicMock())
+ telephone.active_call = SimpleNamespace(
+ profile=LXSTTelephony.Profiles.QUALITY_MEDIUM,
+ filters=[],
+ packetizer=MagicMock(),
+ )
telephone.transmit_mixer = _FakeMixer(target_frame_ms=60, gain=0.0)
telephone.audio_input = _FakeLineSource()
telephone.transmit_pipeline = _FakePipeline()

diff --git a/tests/backend/test_rns_lifecycle.py b/tests/backend/test_rns_lifecycle.py
index 4b9e3c67..dfec552d 100644
--- a/tests/backend/test_rns_lifecycle.py
+++ b/tests/backend/test_rns_lifecycle.py
@@ -1,4 +1,5 @@
import os
+import json
import shutil
import tempfile
from unittest.mock import AsyncMock, MagicMock, patch
@@ -253,7 +254,7 @@ async def test_reload_reticulum_failure_recovery(mock_rns, temp_dir):
# or just mock a method inside the try block to raise.
with patch.object(
app,
- "teardown_identity",
+ "_teardown_all_contexts_for_reload",
side_effect=Exception("Reload failed"),
):
result = await app.reload_reticulum()
@@ -312,3 +313,378 @@ async def test_hotswap_identity(mock_rns, temp_dir):
broadcast_call = app.websocket_broadcast.call_args[0][0]
assert "identity_switched" in broadcast_call
app.teardown_identity()
+
+
+@pytest.mark.asyncio
+async def test_reload_reticulum_restores_same_identity(mock_rns, temp_dir):
+ with (
+ patch("meshchatx.src.backend.identity_context.Database"),
+ patch("meshchatx.src.backend.identity_context.ConfigManager"),
+ patch("meshchatx.src.backend.identity_context.MessageHandler"),
+ patch("meshchatx.src.backend.identity_context.AnnounceManager"),
+ patch("meshchatx.src.backend.identity_context.ArchiverManager"),
+ patch("meshchatx.src.backend.identity_context.MapManager"),
+ patch("meshchatx.src.backend.identity_context.TelephoneManager"),
+ patch("meshchatx.src.backend.identity_context.VoicemailManager"),
+ patch("meshchatx.src.backend.identity_context.RingtoneManager"),
+ patch("meshchatx.src.backend.identity_context.RNCPHandler"),
+ patch("meshchatx.src.backend.identity_context.RNStatusHandler"),
+ patch("meshchatx.src.backend.identity_context.RNProbeHandler"),
+ patch("meshchatx.src.backend.identity_context.TranslatorHandler"),
+ patch("LXMF.LXMRouter"),
+ patch("asyncio.sleep", return_value=None),
+ patch("socket.socket") as mock_socket,
+ ):
+ mock_sock_inst = MagicMock()
+ mock_socket.return_value = mock_sock_inst
+
+ app = ReticulumMeshChat(
+ identity=mock_rns["id_instance"],
+ storage_dir=temp_dir,
+ reticulum_config_dir=temp_dir,
+ )
+ original_identity = app.identity
+ app.setup_identity = MagicMock()
+ app.cleanup_rns_state_for_identity = MagicMock()
+
+ result = await app.reload_reticulum()
+
+ assert result is True
+ app.cleanup_rns_state_for_identity.assert_called_with(original_identity.hash)
+ app.setup_identity.assert_called_with(original_identity)
+ app.teardown_identity()
+
+
+@pytest.mark.asyncio
+async def test_transport_enable_endpoint_reloads_rns(mock_rns, temp_dir):
+ with (
+ patch("meshchatx.src.backend.identity_context.Database"),
+ patch("meshchatx.src.backend.identity_context.ConfigManager"),
+ patch("meshchatx.src.backend.identity_context.MessageHandler"),
+ patch("meshchatx.src.backend.identity_context.AnnounceManager"),
+ patch("meshchatx.src.backend.identity_context.ArchiverManager"),
+ patch("meshchatx.src.backend.identity_context.MapManager"),
+ patch("meshchatx.src.backend.identity_context.TelephoneManager"),
+ patch("meshchatx.src.backend.identity_context.VoicemailManager"),
+ patch("meshchatx.src.backend.identity_context.RingtoneManager"),
+ patch("meshchatx.src.backend.identity_context.RNCPHandler"),
+ patch("meshchatx.src.backend.identity_context.RNStatusHandler"),
+ patch("meshchatx.src.backend.identity_context.RNProbeHandler"),
+ patch("meshchatx.src.backend.identity_context.TranslatorHandler"),
+ patch("LXMF.LXMRouter"),
+ ):
+ app = ReticulumMeshChat(
+ identity=mock_rns["id_instance"],
+ storage_dir=temp_dir,
+ reticulum_config_dir=temp_dir,
+ )
+ app.reload_reticulum = AsyncMock(return_value=True)
+
+ handler = None
+ for route in app.get_routes():
+ if (
+ route.path == "/api/v1/reticulum/enable-transport"
+ and route.method == "POST"
+ ):
+ handler = route.handler
+ break
+
+ assert handler is not None
+
+ response = await handler(MagicMock())
+ payload = json.loads(response.body)
+
+ assert response.status == 200
+ assert (
+ payload["message"]
+ == "Transport mode enabled and RNS restarted successfully."
+ )
+ app.reload_reticulum.assert_awaited_once()
+ app.teardown_identity()
+
+
+@pytest.mark.asyncio
+async def test_transport_disable_endpoint_reloads_rns(mock_rns, temp_dir):
+ with (
+ patch("meshchatx.src.backend.identity_context.Database"),
+ patch("meshchatx.src.backend.identity_context.ConfigManager"),
+ patch("meshchatx.src.backend.identity_context.MessageHandler"),
+ patch("meshchatx.src.backend.identity_context.AnnounceManager"),
+ patch("meshchatx.src.backend.identity_context.ArchiverManager"),
+ patch("meshchatx.src.backend.identity_context.MapManager"),
+ patch("meshchatx.src.backend.identity_context.TelephoneManager"),
+ patch("meshchatx.src.backend.identity_context.VoicemailManager"),
+ patch("meshchatx.src.backend.identity_context.RingtoneManager"),
+ patch("meshchatx.src.backend.identity_context.RNCPHandler"),
+ patch("meshchatx.src.backend.identity_context.RNStatusHandler"),
+ patch("meshchatx.src.backend.identity_context.RNProbeHandler"),
+ patch("meshchatx.src.backend.identity_context.TranslatorHandler"),
+ patch("LXMF.LXMRouter"),
+ ):
+ app = ReticulumMeshChat(
+ identity=mock_rns["id_instance"],
+ storage_dir=temp_dir,
+ reticulum_config_dir=temp_dir,
+ )
+ app.reload_reticulum = AsyncMock(return_value=True)
+
+ handler = None
+ for route in app.get_routes():
+ if (
+ route.path == "/api/v1/reticulum/disable-transport"
+ and route.method == "POST"
+ ):
+ handler = route.handler
+ break
+
+ assert handler is not None
+
+ response = await handler(MagicMock())
+ payload = json.loads(response.body)
+
+ assert response.status == 200
+ assert (
+ payload["message"]
+ == "Transport mode disabled and RNS restarted successfully."
+ )
+ app.reload_reticulum.assert_awaited_once()
+ app.teardown_identity()
+
+
+@pytest.mark.asyncio
+async def test_transport_enable_endpoint_reload_failure(mock_rns, temp_dir):
+ with (
+ patch("meshchatx.src.backend.identity_context.Database"),
+ patch("meshchatx.src.backend.identity_context.ConfigManager"),
+ patch("meshchatx.src.backend.identity_context.MessageHandler"),
+ patch("meshchatx.src.backend.identity_context.AnnounceManager"),
+ patch("meshchatx.src.backend.identity_context.ArchiverManager"),
+ patch("meshchatx.src.backend.identity_context.MapManager"),
+ patch("meshchatx.src.backend.identity_context.TelephoneManager"),
+ patch("meshchatx.src.backend.identity_context.VoicemailManager"),
+ patch("meshchatx.src.backend.identity_context.RingtoneManager"),
+ patch("meshchatx.src.backend.identity_context.RNCPHandler"),
+ patch("meshchatx.src.backend.identity_context.RNStatusHandler"),
+ patch("meshchatx.src.backend.identity_context.RNProbeHandler"),
+ patch("meshchatx.src.backend.identity_context.TranslatorHandler"),
+ patch("LXMF.LXMRouter"),
+ ):
+ app = ReticulumMeshChat(
+ identity=mock_rns["id_instance"],
+ storage_dir=temp_dir,
+ reticulum_config_dir=temp_dir,
+ )
+ app.reload_reticulum = AsyncMock(return_value=False)
+
+ handler = None
+ for route in app.get_routes():
+ if (
+ route.path == "/api/v1/reticulum/enable-transport"
+ and route.method == "POST"
+ ):
+ handler = route.handler
+ break
+
+ assert handler is not None
+
+ response = await handler(MagicMock())
+ payload = json.loads(response.body)
+
+ assert response.status == 500
+ assert (
+ payload["message"]
+ == "Transport mode was enabled in config, but RNS reload failed."
+ )
+ app.reload_reticulum.assert_awaited_once()
+ app.teardown_identity()
+
+
+@pytest.mark.asyncio
+async def test_transport_disable_endpoint_reload_failure(mock_rns, temp_dir):
+ with (
+ patch("meshchatx.src.backend.identity_context.Database"),
+ patch("meshchatx.src.backend.identity_context.ConfigManager"),
+ patch("meshchatx.src.backend.identity_context.MessageHandler"),
+ patch("meshchatx.src.backend.identity_context.AnnounceManager"),
+ patch("meshchatx.src.backend.identity_context.ArchiverManager"),
+ patch("meshchatx.src.backend.identity_context.MapManager"),
+ patch("meshchatx.src.backend.identity_context.TelephoneManager"),
+ patch("meshchatx.src.backend.identity_context.VoicemailManager"),
+ patch("meshchatx.src.backend.identity_context.RingtoneManager"),
+ patch("meshchatx.src.backend.identity_context.RNCPHandler"),
+ patch("meshchatx.src.backend.identity_context.RNStatusHandler"),
+ patch("meshchatx.src.backend.identity_context.RNProbeHandler"),
+ patch("meshchatx.src.backend.identity_context.TranslatorHandler"),
+ patch("LXMF.LXMRouter"),
+ ):
+ app = ReticulumMeshChat(
+ identity=mock_rns["id_instance"],
+ storage_dir=temp_dir,
+ reticulum_config_dir=temp_dir,
+ )
+ app.reload_reticulum = AsyncMock(return_value=False)
+
+ handler = None
+ for route in app.get_routes():
+ if (
+ route.path == "/api/v1/reticulum/disable-transport"
+ and route.method == "POST"
+ ):
+ handler = route.handler
+ break
+
+ assert handler is not None
+
+ response = await handler(MagicMock())
+ payload = json.loads(response.body)
+
+ assert response.status == 500
+ assert (
+ payload["message"]
+ == "Transport mode was disabled in config, but RNS reload failed."
+ )
+ app.reload_reticulum.assert_awaited_once()
+ app.teardown_identity()
+
+
+@pytest.mark.asyncio
+async def test_reticulum_reload_endpoint_success(mock_rns, temp_dir):
+ with (
+ patch("meshchatx.src.backend.identity_context.Database"),
+ patch("meshchatx.src.backend.identity_context.ConfigManager"),
+ patch("meshchatx.src.backend.identity_context.MessageHandler"),
+ patch("meshchatx.src.backend.identity_context.AnnounceManager"),
+ patch("meshchatx.src.backend.identity_context.ArchiverManager"),
+ patch("meshchatx.src.backend.identity_context.MapManager"),
+ patch("meshchatx.src.backend.identity_context.TelephoneManager"),
+ patch("meshchatx.src.backend.identity_context.VoicemailManager"),
+ patch("meshchatx.src.backend.identity_context.RingtoneManager"),
+ patch("meshchatx.src.backend.identity_context.RNCPHandler"),
+ patch("meshchatx.src.backend.identity_context.RNStatusHandler"),
+ patch("meshchatx.src.backend.identity_context.RNProbeHandler"),
+ patch("meshchatx.src.backend.identity_context.TranslatorHandler"),
+ patch("LXMF.LXMRouter"),
+ ):
+ app = ReticulumMeshChat(
+ identity=mock_rns["id_instance"],
+ storage_dir=temp_dir,
+ reticulum_config_dir=temp_dir,
+ )
+ app.reload_reticulum = AsyncMock(return_value=True)
+
+ handler = None
+ for route in app.get_routes():
+ if route.path == "/api/v1/reticulum/reload" and route.method == "POST":
+ handler = route.handler
+ break
+
+ assert handler is not None
+
+ response = await handler(MagicMock())
+ payload = json.loads(response.body)
+
+ assert response.status == 200
+ assert payload["message"] == "Reticulum reloaded successfully"
+ app.reload_reticulum.assert_awaited_once()
+ app.teardown_identity()
+
+
+@pytest.mark.asyncio
+async def test_reticulum_reload_endpoint_failure(mock_rns, temp_dir):
+ with (
+ patch("meshchatx.src.backend.identity_context.Database"),
+ patch("meshchatx.src.backend.identity_context.ConfigManager"),
+ patch("meshchatx.src.backend.identity_context.MessageHandler"),
+ patch("meshchatx.src.backend.identity_context.AnnounceManager"),
+ patch("meshchatx.src.backend.identity_context.ArchiverManager"),
+ patch("meshchatx.src.backend.identity_context.MapManager"),
+ patch("meshchatx.src.backend.identity_context.TelephoneManager"),
+ patch("meshchatx.src.backend.identity_context.VoicemailManager"),
+ patch("meshchatx.src.backend.identity_context.RingtoneManager"),
+ patch("meshchatx.src.backend.identity_context.RNCPHandler"),
+ patch("meshchatx.src.backend.identity_context.RNStatusHandler"),
+ patch("meshchatx.src.backend.identity_context.RNProbeHandler"),
+ patch("meshchatx.src.backend.identity_context.TranslatorHandler"),
+ patch("LXMF.LXMRouter"),
+ ):
+ app = ReticulumMeshChat(
+ identity=mock_rns["id_instance"],
+ storage_dir=temp_dir,
+ reticulum_config_dir=temp_dir,
+ )
+ app.reload_reticulum = AsyncMock(return_value=False)
+
+ handler = None
+ for route in app.get_routes():
+ if route.path == "/api/v1/reticulum/reload" and route.method == "POST":
+ handler = route.handler
+ break
+
+ assert handler is not None
+
+ response = await handler(MagicMock())
+ payload = json.loads(response.body)
+
+ assert response.status == 500
+ assert payload["error"] == "Failed to reload Reticulum"
+ app.reload_reticulum.assert_awaited_once()
+ app.teardown_identity()
+
+
+@pytest.mark.asyncio
+async def test_reload_teardown_stops_all_context_services(mock_rns, temp_dir):
+ with (
+ patch("meshchatx.src.backend.identity_context.Database"),
+ patch("meshchatx.src.backend.identity_context.ConfigManager"),
+ patch("meshchatx.src.backend.identity_context.MessageHandler"),
+ patch("meshchatx.src.backend.identity_context.AnnounceManager"),
+ patch("meshchatx.src.backend.identity_context.ArchiverManager"),
+ patch("meshchatx.src.backend.identity_context.MapManager"),
+ patch("meshchatx.src.backend.identity_context.TelephoneManager"),
+ patch("meshchatx.src.backend.identity_context.VoicemailManager"),
+ patch("meshchatx.src.backend.identity_context.RingtoneManager"),
+ patch("meshchatx.src.backend.identity_context.RNCPHandler"),
+ patch("meshchatx.src.backend.identity_context.RNStatusHandler"),
+ patch("meshchatx.src.backend.identity_context.RNProbeHandler"),
+ patch("meshchatx.src.backend.identity_context.TranslatorHandler"),
+ patch("LXMF.LXMRouter"),
+ ):
+ app = ReticulumMeshChat(
+ identity=mock_rns["id_instance"],
+ storage_dir=temp_dir,
+ reticulum_config_dir=temp_dir,
+ )
+
+ identity_a = MagicMock()
+ identity_a.hash = b"a" * 16
+ identity_b = MagicMock()
+ identity_b.hash = b"b" * 16
+
+ ctx_a = MagicMock()
+ ctx_a.identity = identity_a
+ ctx_a.bot_handler = MagicMock()
+ ctx_a.identity_hash = identity_a.hash.hex()
+
+ ctx_b = MagicMock()
+ ctx_b.identity = identity_b
+ ctx_b.bot_handler = MagicMock()
+ ctx_b.identity_hash = identity_b.hash.hex()
+
+ app.contexts = {
+ ctx_a.identity_hash: ctx_a,
+ ctx_b.identity_hash: ctx_b,
+ }
+ app.current_context = ctx_a
+ app.stop_local_propagation_node = MagicMock()
+ app.page_node_manager.teardown = MagicMock()
+
+ app._teardown_all_contexts_for_reload()
+
+ ctx_a.bot_handler.stop_all.assert_called_once()
+ ctx_b.bot_handler.stop_all.assert_called_once()
+ assert app.stop_local_propagation_node.call_count == 2
+ app.page_node_manager.teardown.assert_called_once()
+ ctx_a.teardown.assert_called_once()
+ ctx_b.teardown.assert_called_once()
+ assert app.contexts == {}
+ assert app.current_context is None

diff --git a/tests/backend/test_telephone_initiation.py b/tests/backend/test_telephone_initiation.py
index eb0a5b94..8d1633b8 100644
--- a/tests/backend/test_telephone_initiation.py
+++ b/tests/backend/test_telephone_initiation.py
@@ -40,9 +40,17 @@ async def test_initiate_retries_path_requests_during_lookup(telephone_manager):
)
with (
- patch("meshchatx.src.backend.telephone_manager.RNS.Identity.recall", return_value=MagicMock()),
- patch("meshchatx.src.backend.telephone_manager.RNS.Transport.has_path", side_effect=has_path),
- patch("meshchatx.src.backend.telephone_manager.RNS.Transport.request_path") as request_path,
+ patch(
+ "meshchatx.src.backend.telephone_manager.RNS.Identity.recall",
+ return_value=MagicMock(),
+ ),
+ patch(
+ "meshchatx.src.backend.telephone_manager.RNS.Transport.has_path",
+ side_effect=has_path,
+ ),
+ patch(
+ "meshchatx.src.backend.telephone_manager.RNS.Transport.request_path"
+ ) as request_path,
):
await telephone_manager.initiate(destination_hash, timeout_seconds=1)
@@ -60,14 +68,22 @@ async def test_initiate_cancels_quickly_while_finding_path_identity(telephone_ma
telephone_manager._update_initiation_status(None, None)
with (
- patch("meshchatx.src.backend.telephone_manager.RNS.Identity.recall", return_value=None),
- patch("meshchatx.src.backend.telephone_manager.RNS.Transport.has_path", return_value=False),
+ patch(
+ "meshchatx.src.backend.telephone_manager.RNS.Identity.recall",
+ return_value=None,
+ ),
+ patch(
+ "meshchatx.src.backend.telephone_manager.RNS.Transport.has_path",
+ return_value=False,
+ ),
patch(
"meshchatx.src.backend.telephone_manager.RNS.Transport.request_path",
side_effect=request_path_and_cancel,
),
):
- task = asyncio.create_task(telephone_manager.initiate(destination_hash, timeout_seconds=5))
+ task = asyncio.create_task(
+ telephone_manager.initiate(destination_hash, timeout_seconds=5)
+ )
result = await asyncio.wait_for(task, timeout=0.3)
assert result is None
@@ -84,12 +100,23 @@ async def test_initiate_cancels_quickly_while_dialling(telephone_manager):
telephone_manager.telephone.call.side_effect = blocking_call
with (
- patch("meshchatx.src.backend.telephone_manager.RNS.Identity.recall", return_value=MagicMock()),
- patch("meshchatx.src.backend.telephone_manager.RNS.Transport.has_path", return_value=True),
+ patch(
+ "meshchatx.src.backend.telephone_manager.RNS.Identity.recall",
+ return_value=MagicMock(),
+ ),
+ patch(
+ "meshchatx.src.backend.telephone_manager.RNS.Transport.has_path",
+ return_value=True,
+ ),
):
- task = asyncio.create_task(telephone_manager.initiate(destination_hash, timeout_seconds=5))
+ task = asyncio.create_task(
+ telephone_manager.initiate(destination_hash, timeout_seconds=5)
+ )
for _ in range(200):
- if telephone_manager.initiation_status in ("Establishing link...", "Calling..."):
+ if telephone_manager.initiation_status in (
+ "Establishing link...",
+ "Calling...",
+ ):
break
await asyncio.sleep(0)
@@ -149,7 +176,9 @@ async def test_cancel_after_path_found_before_dialling_stabilizes(telephone_mana
return_value=True,
),
):
- task = asyncio.create_task(telephone_manager.initiate(destination_hash, timeout_seconds=2))
+ task = asyncio.create_task(
+ telephone_manager.initiate(destination_hash, timeout_seconds=2)
+ )
for _ in range(200):
if telephone_manager.initiation_status == "Establishing link...":
break
@@ -254,7 +283,9 @@ async def test_call_thread_exception_surfaces_without_hanging(telephone_manager)
"meshchatx.src.backend.telephone_manager.RNS.Transport.has_path",
return_value=True,
),
- patch("meshchatx.src.backend.telephone_manager.asyncio.sleep", side_effect=no_wait),
+ patch(
+ "meshchatx.src.backend.telephone_manager.asyncio.sleep", side_effect=no_wait
+ ),
):
result = await asyncio.wait_for(
telephone_manager.initiate(destination_hash, timeout_seconds=1),
@@ -274,6 +305,7 @@ async def test_inconsistent_call_status_finishes_within_timeout(telephone_manage
return None
telephone_manager.telephone.call.side_effect = inconsistent_call
+
async def no_wait(_seconds):
return None
@@ -286,7 +318,9 @@ async def test_inconsistent_call_status_finishes_within_timeout(telephone_manage
"meshchatx.src.backend.telephone_manager.RNS.Transport.has_path",
return_value=True,
),
- patch("meshchatx.src.backend.telephone_manager.asyncio.sleep", side_effect=no_wait),
+ patch(
+ "meshchatx.src.backend.telephone_manager.asyncio.sleep", side_effect=no_wait
+ ),
):
result = await asyncio.wait_for(
telephone_manager.initiate(destination_hash, timeout_seconds=0.2),
@@ -339,8 +373,10 @@ async def test_lxst_busy_and_rejected_end_without_stuck_status(telephone_manager
for terminal_state in (0, 1):
telephone_manager._status_events.clear()
telephone_manager.telephone.call_status = 3
- telephone_manager.telephone.call.side_effect = lambda _identity, state=terminal_state: setattr(
- telephone_manager.telephone, "call_status", state
+ telephone_manager.telephone.call.side_effect = (
+ lambda _identity, state=terminal_state: setattr(
+ telephone_manager.telephone, "call_status", state
+ )
)
with (
@@ -386,7 +422,9 @@ async def test_rapid_dial_cancel_soak_has_bounded_memory(telephone_manager):
tracemalloc.start()
for _ in range(loops):
telephone_manager.telephone.call_status = 3
- task = asyncio.create_task(telephone_manager.initiate(destination_hash, timeout_seconds=1))
+ task = asyncio.create_task(
+ telephone_manager.initiate(destination_hash, timeout_seconds=1)
+ )
await asyncio.sleep(0.005)
telephone_manager._update_initiation_status(None, None)
await asyncio.wait_for(task, timeout=0.5)

diff --git a/tests/e2e/helpers.js b/tests/e2e/helpers.js
index 55bf3137..b8ac2f7a 100644
--- a/tests/e2e/helpers.js
+++ b/tests/e2e/helpers.js
@@ -1,8 +1,81 @@
+const crypto = require("crypto");
const { expect } = require("@playwright/test");
const E2E_BACKEND_PORT = process.env.E2E_BACKEND_PORT || "18079";
const E2E_BACKEND_ORIGIN = `http://127.0.0.1:${E2E_BACKEND_PORT}`;
+const E2E_SCROLL_PEER_HASH = `e2e0${"0".repeat(28)}`;
+
+function buildE2eLxmfRow({ peerHash, localHash, index, total, inbound }) {
+ const hash = crypto.randomBytes(16).toString("hex");
+ const baseTs = Math.floor(Date.now() / 1000) - total;
+ return {
+ hash,
+ source_hash: inbound ? peerHash : localHash,
+ destination_hash: inbound ? localHash : peerHash,
+ peer_hash: peerHash,
+ state: "delivered",
+ progress: 1.0,
+ is_incoming: inbound ? 1 : 0,
+ method: "direct",
+ delivery_attempts: 1,
+ next_delivery_attempt_at: null,
+ title: "",
+ content: `E2E scroll seed ${String(index).padStart(3, "0")} ${"x".repeat(64)}`,
+ fields: "{}",
+ timestamp: baseTs + index,
+ rssi: null,
+ snr: null,
+ quality: null,
+ is_spam: 0,
+ reply_to_hash: null,
+ attachments_stripped: 0,
+ };
+}
+
+/**
+ * @param {import('@playwright/test').APIRequestContext} request
+ * @returns {Promise<string>}
+ */
+async function getE2eLocalLxmfHash(request) {
+ const cfgRes = await request.get(`${E2E_BACKEND_ORIGIN}/api/v1/config`);
+ expect(cfgRes.ok()).toBeTruthy();
+ const cfgBody = await cfgRes.json();
+ const localHash = cfgBody.config?.lxmf_address_hash;
+ expect(localHash && String(localHash).length === 32).toBeTruthy();
+ return localHash;
+}
+
+/**
+ * Inserts LXMF rows via maintenance import so the messages UI has a long thread for scroll tests.
+ * @param {import('@playwright/test').APIRequestContext} request
+ * @param {{ messageCount?: number }} [opts]
+ * @returns {Promise<{ peerHash: string, localHash: string }>}
+ */
+async function seedE2eLongConversationThread(request, opts = {}) {
+ const messageCount = opts.messageCount ?? 45;
+ const localHash = await getE2eLocalLxmfHash(request);
+
+ const peerHash = E2E_SCROLL_PEER_HASH;
+ const messages = [];
+ for (let i = 0; i < messageCount; i++) {
+ messages.push(
+ buildE2eLxmfRow({
+ peerHash,
+ localHash,
+ index: i,
+ total: messageCount,
+ inbound: i % 2 === 0,
+ })
+ );
+ }
+ const imp = await request.post(`${E2E_BACKEND_ORIGIN}/api/v1/maintenance/messages/import`, {
+ data: { messages },
+ });
+ expect(imp.ok()).toBeTruthy();
+ return { peerHash, localHash };
+}
+
const PALETTE_PLACEHOLDER = /Search commands,\s*(routes|navigate),\s*or peers\.{0,3}/i;
/**
@@ -51,8 +124,11 @@ async function dismissMapOnboardingTooltip(page) {
module.exports = {
E2E_BACKEND_ORIGIN,
+ E2E_SCROLL_PEER_HASH,
PALETTE_PLACEHOLDER,
dismissMapOnboardingTooltip,
openCommandPalette,
prepareE2eSession,
+ getE2eLocalLxmfHash,
+ seedE2eLongConversationThread,
};

diff --git a/tests/e2e/messages-conversation-scroll.spec.js b/tests/e2e/messages-conversation-scroll.spec.js
index 48f36cd5..a40f5e40 100644
--- a/tests/e2e/messages-conversation-scroll.spec.js
+++ b/tests/e2e/messages-conversation-scroll.spec.js
@@ -82,9 +82,18 @@ test.describe("Messages conversation scroll", () => {
test("starts near bottom with a long thread", async ({ page }) => {
await page.goto("/#/messages");
await expect(page.getByText("Conversations", { exact: true }).first()).toBeVisible({ timeout: 25000 });
- await page.locator(".conversation-item").filter({ hasText: /E2E scroll seed/ }).first().click();
+ await page
+ .locator(".conversation-item")
+ .filter({ hasText: /E2E scroll seed/ })
+ .first()
+ .click();
await expect(page.locator("#messages")).toBeVisible({ timeout: 25000 });
- await expect(page.locator("#messages").getByText(/E2E scroll seed 119/).first()).toBeVisible({
+ await expect(
+ page
+ .locator("#messages")
+ .getByText(/E2E scroll seed 119/)
+ .first()
+ ).toBeVisible({
timeout: 25000,
});
@@ -98,7 +107,11 @@ test.describe("Messages conversation scroll", () => {
const localHash = await getE2eLocalLxmfHash(request);
await page.goto("/#/messages");
await expect(page.getByText("Conversations", { exact: true }).first()).toBeVisible({ timeout: 25000 });
- await page.locator(".conversation-item").filter({ hasText: /E2E scroll seed/ }).first().click();
+ await page
+ .locator(".conversation-item")
+ .filter({ hasText: /E2E scroll seed/ })
+ .first()
+ .click();
await expect(page.locator("#messages")).toBeVisible({ timeout: 25000 });
await waitForMessagesOverflow(page);
@@ -145,7 +158,11 @@ test.describe("Messages conversation scroll", () => {
const localHash = await getE2eLocalLxmfHash(request);
await page.goto("/#/messages");
await expect(page.getByText("Conversations", { exact: true }).first()).toBeVisible({ timeout: 25000 });
- await page.locator(".conversation-item").filter({ hasText: /E2E scroll seed/ }).first().click();
+ await page
+ .locator(".conversation-item")
+ .filter({ hasText: /E2E scroll seed/ })
+ .first()
+ .click();
await expect(page.locator("#messages")).toBeVisible({ timeout: 25000 });
await waitForMessagesOverflow(page);
@@ -183,7 +200,11 @@ test.describe("Messages conversation scroll", () => {
test("preserves scroll anchor when loading older messages from the top", async ({ page }) => {
await page.goto("/#/messages");
await expect(page.getByText("Conversations", { exact: true }).first()).toBeVisible({ timeout: 25000 });
- await page.locator(".conversation-item").filter({ hasText: /E2E scroll seed/ }).first().click();
+ await page
+ .locator(".conversation-item")
+ .filter({ hasText: /E2E scroll seed/ })
+ .first()
+ .click();
await expect(page.locator("#messages")).toBeVisible({ timeout: 25000 });
await waitForMessagesOverflow(page);

diff --git a/tests/frontend/ConversationViewerPerformance.baseline.test.js b/tests/frontend/ConversationViewerPerformance.baseline.test.js
index 57f2da0d..52538021 100644
--- a/tests/frontend/ConversationViewerPerformance.baseline.test.js
+++ b/tests/frontend/ConversationViewerPerformance.baseline.test.js
@@ -104,77 +104,65 @@ describe("ConversationViewer performance baselines", () => {
expect(sig[0]).toMatch(/^single:msg_/);
});
- it(
- "bulk chatItems update: baseline ceiling (detect regressions)",
- async () => {
- const wrapper = mountViewer();
- const n = 800;
- const items = makeChatItems(n, myLxmfAddressHash, peerHash);
-
- const t0 = performance.now();
- await wrapper.setData({ chatItems: items });
- await wrapper.vm.$nextTick();
- const ms = performance.now() - t0;
-
- expect(wrapper.vm.selectedPeerChatDisplayGroups.length).toBe(n);
- expect(ms).toBeLessThan(15000);
- },
- 60_000
- );
-
- it(
- "incremental append: baseline ceiling when thread already large",
- async () => {
- const wrapper = mountViewer();
- const n = 600;
- await wrapper.setData({ chatItems: makeChatItems(n, myLxmfAddressHash, peerHash) });
- await wrapper.vm.$nextTick();
-
- const newMsg = {
- type: "lxmf_message",
- is_outbound: true,
- lxmf_message: {
- hash: "newmsg".padEnd(32, "0"),
- source_hash: myLxmfAddressHash,
- destination_hash: peerHash,
- content: "New",
- created_at: new Date().toISOString(),
- state: "delivered",
- method: "direct",
- progress: 1.0,
- delivery_attempts: 1,
- id: n,
- },
- };
+ it("bulk chatItems update: baseline ceiling (detect regressions)", async () => {
+ const wrapper = mountViewer();
+ const n = 800;
+ const items = makeChatItems(n, myLxmfAddressHash, peerHash);
- const t0 = performance.now();
- wrapper.vm.chatItems.push(newMsg);
- await wrapper.vm.$nextTick();
- const ms = performance.now() - t0;
+ const t0 = performance.now();
+ await wrapper.setData({ chatItems: items });
+ await wrapper.vm.$nextTick();
+ const ms = performance.now() - t0;
- expect(wrapper.vm.selectedPeerChatDisplayGroups.length).toBe(n + 1);
- expect(ms).toBeLessThan(8000);
- },
- 60_000
- );
-
- it(
- "display groups computation alone stays bounded for large n",
- async () => {
- const wrapper = mountViewer();
- const n = 2000;
- await wrapper.setData({ chatItems: makeChatItems(n, myLxmfAddressHash, peerHash) });
- await wrapper.vm.$nextTick();
-
- const t0 = performance.now();
- for (let k = 0; k < 20; k++) {
- void wrapper.vm.selectedPeerChatDisplayGroups;
- }
- const ms = performance.now() - t0;
-
- expect(wrapper.vm.selectedPeerChatDisplayGroups.length).toBe(n);
- expect(ms).toBeLessThan(2000);
- },
- 60_000
- );
+ expect(wrapper.vm.selectedPeerChatDisplayGroups.length).toBe(n);
+ expect(ms).toBeLessThan(15000);
+ }, 60_000);
+
+ it("incremental append: baseline ceiling when thread already large", async () => {
+ const wrapper = mountViewer();
+ const n = 600;
+ await wrapper.setData({ chatItems: makeChatItems(n, myLxmfAddressHash, peerHash) });
+ await wrapper.vm.$nextTick();
+
+ const newMsg = {
+ type: "lxmf_message",
+ is_outbound: true,
+ lxmf_message: {
+ hash: "newmsg".padEnd(32, "0"),
+ source_hash: myLxmfAddressHash,
+ destination_hash: peerHash,
+ content: "New",
+ created_at: new Date().toISOString(),
+ state: "delivered",
+ method: "direct",
+ progress: 1.0,
+ delivery_attempts: 1,
+ id: n,
+ },
+ };
+
+ const t0 = performance.now();
+ wrapper.vm.chatItems.push(newMsg);
+ await wrapper.vm.$nextTick();
+ const ms = performance.now() - t0;
+
+ expect(wrapper.vm.selectedPeerChatDisplayGroups.length).toBe(n + 1);
+ expect(ms).toBeLessThan(8000);
+ }, 60_000);
+
+ it("display groups computation alone stays bounded for large n", async () => {
+ const wrapper = mountViewer();
+ const n = 2000;
+ await wrapper.setData({ chatItems: makeChatItems(n, myLxmfAddressHash, peerHash) });
+ await wrapper.vm.$nextTick();
+
+ const t0 = performance.now();
+ for (let k = 0; k < 20; k++) {
+ void wrapper.vm.selectedPeerChatDisplayGroups;
+ }
+ const ms = performance.now() - t0;
+
+ expect(wrapper.vm.selectedPeerChatDisplayGroups.length).toBe(n);
+ expect(ms).toBeLessThan(2000);
+ }, 60_000);
});

diff --git a/tests/frontend/KeyboardShortcuts.test.js b/tests/frontend/KeyboardShortcuts.test.js
index b78c64ac..f0a59289 100644
--- a/tests/frontend/KeyboardShortcuts.test.js
+++ b/tests/frontend/KeyboardShortcuts.test.js
@@ -126,7 +126,7 @@ describe("KeyboardShortcuts", () => {
type: "keyboard_shortcuts.set",
action: "nav_messages",
keys: ["alt", "q"],
- }),
+ })
);
});
@@ -136,7 +136,7 @@ describe("KeyboardShortcuts", () => {
JSON.stringify({
type: "keyboard_shortcuts.delete",
action: "nav_map",
- }),
+ })
);
});
});

diff --git a/tests/frontend/SettingsPage.config-persistence.test.js b/tests/frontend/SettingsPage.config-persistence.test.js
index 9c8c79c8..f8534379 100644
--- a/tests/frontend/SettingsPage.config-persistence.test.js
+++ b/tests/frontend/SettingsPage.config-persistence.test.js
@@ -300,7 +300,7 @@ describe("SettingsPage — config persistence (PATCH and related)", () => {
message_inbound_bubble_color: null,
message_failed_bubble_color: "#ef4444",
message_waiting_bubble_color: "#e5e7eb",
- }),
+ })
);
});
@@ -330,7 +330,7 @@ describe("SettingsPage — config persistence (PATCH and related)", () => {
expect.objectContaining({
announce_max_stored_lxmf_delivery: 900,
discovered_interfaces_max_return: 500,
- }),
+ })
);
});
@@ -341,21 +341,21 @@ describe("SettingsPage — config persistence (PATCH and related)", () => {
"/api/v1/config",
expect.objectContaining({
auto_resend_failed_messages_when_announce_received: true,
- }),
+ })
);
await w.vm.onAllowAutoResendingFailedMessagesWithAttachmentsChange();
expect(api.patch).toHaveBeenCalledWith(
"/api/v1/config",
expect.objectContaining({
allow_auto_resending_failed_messages_with_attachments: false,
- }),
+ })
);
await w.vm.onAutoSendFailedMessagesToPropagationNodeChange();
expect(api.patch).toHaveBeenCalledWith(
"/api/v1/config",
expect.objectContaining({
auto_send_failed_messages_to_propagation_node: false,
- }),
+ })
);
});
@@ -507,7 +507,7 @@ describe("SettingsPage — config persistence (PATCH and related)", () => {
await vi.advanceTimersByTimeAsync(1000);
expect(api.patch).toHaveBeenCalledWith(
"/api/v1/config",
- expect.objectContaining({ banished_text: "OUT", banished_color: "#ff0000" }),
+ expect.objectContaining({ banished_text: "OUT", banished_color: "#ff0000" })
);
});
@@ -574,7 +574,7 @@ describe("SettingsPage — config persistence (PATCH and related)", () => {
expect.objectContaining({
gitea_base_url: "https://gitea.example",
docs_download_urls: "https://docs.example",
- }),
+ })
);
});
@@ -592,7 +592,7 @@ describe("SettingsPage — config persistence (PATCH and related)", () => {
expect.objectContaining({
csp_extra_connect_src: "wss://a.example",
csp_extra_style_src: "https://css.example",
- }),
+ })
);
});
@@ -772,9 +772,7 @@ describe("SettingsPage — maintenance, exports, telemetry trust, RNS reload", (
it("flushArchivedPages sends websocket flush after confirm", async () => {
const w = await mountSettingsPage(api);
await w.vm.flushArchivedPages();
- expect(WebSocketConnection.send).toHaveBeenCalledWith(
- JSON.stringify({ type: "nomadnet.page.archive.flush" }),
- );
+ expect(WebSocketConnection.send).toHaveBeenCalledWith(JSON.stringify({ type: "nomadnet.page.archive.flush" }));
});
it("revokeTelemetryTrust PATCHes contact telemetry flag", async () => {


──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────